/**
 * Created by: Simon McLeish
 * Date: 23-Jul-2010
 * Time: 12:47:34
 * Copyright: London School of Economics 2010
 * Release: 0.1
 */
 
/**
 * This file is part of the Library Locator.

 * The Library Locator is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The Library Locator is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the Library Locator.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * This script is intended for local testing of the overlay of graphics without the servlet functionality for easier fault diagnosis. Graphic 
 * output is saved rather than returned
 *
 * Usage: from commandline type
 * java -classpath lib uk.ac.lse.libraryLocator.mapDisplay.GenerateMapTest --outputfile=file name (w/o extension) to save output to --mapfilemetadata=file name of mapfile metadata --pointerfile=filename of pointer graphic --pointerhotspot=co-ordinates of hotspot in image in form x,y --location=lat,long of position to overlay pointer --bearing=degrees from north to rotate pointer file name of mapfile to use
 */

package uk.ac.lse.libraryLocator.mapDisplay;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.imageio.ImageIO;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderedOp;
import javax.media.jai.TiledImage;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FilenameFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Hashtable;
import java.text.NumberFormat;

public class GenerateMapTest {
  // initialised constants from configuration
  private static String mapFileName, pointerFileName, pointerErrorResolutionProcess;
  private static Integer pointerHotSpotx, pointerHotSpoty;
  private static Hashtable<String,Hashtable<String,Float[]>> fileMetadataHash = new Hashtable<String,Hashtable<String,Float[]>>();
  private static Hashtable<String,String> Params = new Hashtable<String,String>();
  
  public static void main(String[] args){
    try {
      parseArgs(args);
    } catch (IllegalArgumentException e) {
      System.out.println(e.getMessage());
      return;
    }
    pointerFileName = Params.get("pointerfile");
    if (pointerFileName != null && ! "".equals(pointerFileName)){
      String pointerHotSpot = Params.get("pointerhotspot");
      int pos = pointerHotSpot.indexOf(',');
      // pos needs to be not -1 (not found), 0 (first character), or value of length() (last character)
      if (pos > 0 && pos < pointerHotSpot.length()){
        pointerHotSpotx = Integer.parseInt(pointerHotSpot.substring(0,pos));
        pointerHotSpoty = Integer.parseInt(pointerHotSpot.substring(pos+1));
        System.out.println("Pointer hot spot: " + pointerHotSpotx + "," + pointerHotSpoty);
        
        pointerErrorResolutionProcess = "error"; // until we implement something more complicated
        
        try {
          parseMetadataFile(Params.get("mapfilemetadata"));
        }  catch (javax.xml.parsers.ParserConfigurationException e){
          enterLog("Error in parser configuration for metadata document:" + e.getMessage(),1);
          return;
        } catch (SAXException e) {
          enterLog("Error parsing metadata document:" + e.getMessage(),1);
          return;
        } catch (IOException e) {
          enterLog("Error opening metadata document:" + e.getMessage(),1);
          return;
        } catch (Exception e){
          enterLog("No map file information in map metadata file",1);
          return;
        }
        
        enterLog("Loading pointer file " + pointerFileName,4);
        File pointerFile = new File (pointerFileName);
        PlanarImage pointerImage;
        if (pointerFile.exists() && pointerFile.canRead()){
          // load pointer image - error if unable to do this
          pointerImage = JAI.create("fileload",pointerFileName);
        } else {
          // configuration error - pointer file cannot be read
          enterLog("Cannot load pointer file " + pointerFileName,1);
          return;
        }
        
        Integer pointerWidth = pointerImage.getWidth();
        Integer pointerHeight = pointerImage.getHeight();

	// load main image:  GIF, JPEG, PNG, BMP and WBMP,TIFF files supported
	enterLog("Loading main image file",4);
	RenderedImage mainImage = JAI.create("fileload",mapFileName);

	Integer mapWidth = mainImage.getWidth();
	Integer mapHeight = mainImage.getHeight();

	// Load in metadata describing the file
	File mapFile = new File(mapFileName);
	enterLog("Loading image metadata for " + mapFile.getName(),4);
	Hashtable<String,Float[]> mapFileMetadataHash = fileMetadataHash.get(mapFile.getName());

	Integer bearingDeg = 0; // default is not to rotate pointer from north!
	String bearingPar = Params.get("bearing");

	if ((bearingPar != null) && ! "".equals(bearingPar)) {
	  bearingDeg =  Integer.parseInt(bearingPar)%360;
	  // mod 360 as number of degrees - also allows anti-clockwise rotation to be expressed as -n!
	}
	enterLog("Bearing is " + bearingDeg, 4);

	// now calculate orientation (with respect to vertical axis of map) from map file metadata
	double orientation = getOrientation(bearingDeg,mapFileMetadataHash);
	
	String locationStr = Params.get("location");
	
	if ((locationStr != null) && ! "".equals(locationStr)) {
	  // get x and y co-ordinates
	  float locationLongitude, locationLatitude;
	  int cpos = locationStr.indexOf(",");
	  if (cpos > 0 ){
            locationLongitude = Float.parseFloat(locationStr.substring(0,cpos));
            locationLatitude = Float.parseFloat(locationStr.substring(cpos + 1));
          } else {
	    // error - location indicated does not make sense (e.g. is not a pair of numbers)
            enterLog("Location entered does not make sense: " + locationStr,1);
	    return;
	  }

	  // obtain x,y co-ordinates in map file from location information
	  float[] location = getCoordsFromLatLong(locationLongitude,locationLatitude,mapFileMetadataHash,mapWidth,mapHeight);
	  
	  NumberFormat nf = NumberFormat.getNumberInstance();
	  enterLog("Co-ordinates in pixels on map for hotspot are " + nf.format(location[0]) + "," + nf.format(location[1]),4);

	  if (location[0] < 0 || location[0] > mapWidth || location[1] < 0 || location[1] > mapHeight){
	    // location defined but will put pointer outside map completely - so return image with no pointer
	    enterLog("Specified location puts pointer beyond edge of map - returning map with no pointer shown",2);
	    try {
	      writeOutput(mainImage);
	    } catch (IOException e) {
	      enterLog("Unable to output final image: " + e.getMessage(),1);
	      return;
	    }
	    return;
	  }

	  // add main log entry to record request
	  enterLog("Request for map file " + mapFileName + ", orientation " + orientation + "(radians), location " + locationLongitude + "," + locationLatitude, 3);
	
	  // do a bit of trig to see if pointer with given orientation will fit over main image
	  enterLog("Finding edges of rotated pointer - see if it will fit on map",4);
	  double cosOrient = Math.cos(orientation); // calculate once!
	  double sinOrient = Math.sin(orientation); // calculate once!
                     
	  // this compares the map image edge to what was originally the bottom right corner of the pointer image

    // pointerA, pointerB, pointerC and pointerD are the coordinates of the four corners of the pointer
    // graphic relative to the location of the bottom left hand corner of the map graphic (starting 
    // from bottom left and working clockwise)

    double[] pointerA = {location[0] - cosOrient * pointerHotSpotx - sinOrient * pointerHotSpoty, location[1] + sinOrient * pointerHotSpotx - cosOrient * pointerHotSpoty};
    double[] pointerB = {pointerA[0] + sinOrient * pointerHeight, pointerA[1] + cosOrient * pointerHeight};
    double[] pointerC = {pointerB[0] + cosOrient * pointerWidth, pointerB[1] - sinOrient * pointerWidth};
    double[] pointerD = {pointerC[0] - sinOrient * pointerHeight, pointerC[1] - cosOrient  * pointerHeight};

    // if any of these points lie off the map, we're in trouble

	  boolean onMap = true;
    if (pointerA[0] < 0 || pointerA[1] < 0 || pointerB[0] < 0 || pointerB[1] < 0 || pointerC[0] < 0 || pointerC[1] < 0 || pointerD[0] < 0 || pointerD[1] < 0){ onMap = false;}
    if (pointerA[0] > mapWidth || pointerB[0] > mapWidth || pointerC[0] > mapWidth || pointerD[0] > mapWidth){ onMap = false;}
    if (pointerA[1] > mapHeight || pointerB[1] > mapHeight || pointerC[1] > mapHeight || pointerD[1] > mapHeight){ onMap = false;}
          
	  // carry out pointer error resolution
	  if (! onMap){
		  enterLog("Pointer will not fit on map with given orientation",2);
		  enterLog("Carrying out pointer error resolution process " + pointerErrorResolutionProcess,2);
		  if (pointerErrorResolutionProcess.equals("error")){
		    enterLog("Exiting throwing error as specified by configuration",1);
		    // exit
		    return;
		  } else {
	    enterLog("Unsupported pointer error resolution process " + pointerErrorResolutionProcess,1);
	    return;
	  }
        }
	  // rotate pointer image
	  enterLog("Rotating pointer image",4);
	  // angle needs to be Float not double
	  Double orientDouble = new Double(orientation);
	  Float orientFloat = (orientDouble.floatValue());
	  RenderedOp op= JAI.create("rotate", pointerImage, (Object) location[0], location[1], orientFloat);
	  BufferedImage bufferedPointer = op.getAsBufferedImage();
	  enterLog("Pointer image rotation successful",4);
	
	  // overlay on main image - requires pointer image manipulation to ensure compatibility
	  TiledImage mapTI = new TiledImage(mainImage, false);
	  Graphics2D mapG = mapTI.createGraphics();
	  mapG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
	
	  // need to locate bottom left corner of rotated graphic; where this is will depend on how far round the image is rotated (because different
	  // corners of the original graphic will become the lowest and leftmost points)
	
	  enterLog("Calculating position of bottom left corner of rotated image - used to position pointer for overlay",4);
	  Double[] bottomLeft = new Double[] {0.0,0.0};
	  if (0 <= orientation && orientation < (Math.PI / 2)){
	    bottomLeft[0] = location[0] - (sinOrient * pointerHotSpotx) + (sinOrient * pointerHotSpoty);
	    bottomLeft[1] = location[1] - (sinOrient * pointerWidth) + (sinOrient * pointerHotSpoty);
	  } else if (orientation < Math.PI){
	    bottomLeft[0] = location[0] - (sinOrient * pointerHotSpoty) - (cosOrient * (pointerWidth - pointerHotSpotx));
	    bottomLeft[1] = location[1] - (sinOrient * (pointerWidth - pointerHotSpotx) - (cosOrient * (pointerHeight - pointerHotSpoty)));
	  } else if (orientation < (3 * Math.PI / 2)){
	    bottomLeft[0] = location[0] - (cosOrient * (pointerWidth + pointerHeight - pointerHotSpotx - pointerHotSpoty));
	    bottomLeft[1] = location[1] - (sinOrient * (pointerHeight + pointerHotSpotx - pointerHotSpoty));
	  } else {
	    bottomLeft[0] = location[0] + sinOrient * (pointerHeight - pointerHotSpoty);
	    bottomLeft[1] = location[1] - cosOrient * pointerHotSpoty;
	  }
	
	  // create the image
	  enterLog("Overlaying image",4);
	  mapG.drawImage(bufferedPointer, bottomLeft[0].intValue(), bottomLeft[1].intValue(), null);
	  enterLog("Pointer image overlay on map image successful",4);
	
	  // return rendered image
	  enterLog("Outputting overlaid image",4);
	  try {
	    writeOutput(mapTI);
	  } catch (IOException e) {
	    enterLog("Unable to output final image: " + e.getMessage(),1);
	    return;
	  }
	} else {
	  try {
	    writeOutput(mainImage);
	  } catch (IOException e) {
	    enterLog("Unable to output final image: " + e.getMessage(),1);
	    return;
	  }
	}
      } else {
	// no map file found corresponding to request - 404 error
	enterLog("No map file found which corresponds to request",1);
        return;
      }
    }
  }

  private static double getOrientation(Integer bearingDeg, Hashtable<String, Float[]> metadata) {
    // calculate direction of north (in radians) from positions of co-ordinates
    Float[] bottomLeft = metadata.get("bottomleft");
    Float[] topRight = metadata.get("topright");
    if (topRight[1].equals(bottomLeft[1])){
      if (bottomLeft[0] > topRight[0]){
        return (Math.PI / 2); // map diagonal is due east so need to add 90 degrees to get direction of bearing on map
      } else {
        // this won't cope sensibly with a 0x0 map, but such a map doesn't have a sensible north direction either...
        return (- Math.PI / 2);
      }
    } else {
      double tangent = (bottomLeft[0] - topRight[0]) / (topRight[1] - bottomLeft[1]);
      double north = Math.atan(tangent);

      // convert bearing to radians...
	  double bearing = Math.PI * bearingDeg / 180;

      return north + bearing;
    }
  }

    // method for creating a log entry (separated out in case we make it more sophisticated later)
  private static void enterLog(String entry, int level){
    System.out.println(entry);
  }
  
  private static float[] getCoordsFromLatLong (float longitude, float latitude, Hashtable<String,Float[]> metadata, Integer width, Integer height){
    Float[] bottomLeft = metadata.get("bottomleft");
    Float[] topRight = metadata.get("topright");
    float returnVal[] = {0,0};
    returnVal[1] = width * ((longitude - bottomLeft[0]) / (topRight[0] - bottomLeft[0]));
    // need to invert the vertical component because pixels counted from the top
    returnVal[0] = height * (1 - ((latitude - bottomLeft[1]) / (topRight[1] - bottomLeft[1])));
    
    return returnVal;
  }
  
  public static void parseArgs(String[] args) throws IllegalArgumentException {
    System.out.println("No. of arguments: " + args.length);
    for (int i=0; i < args.length -1; i++){
      if (args[i].startsWith("--")){
	String arg = args[i];
	int pos = arg.indexOf('=');
	if (pos > 2){
	  String key = arg.substring(2,pos);
	  String value = arg.substring(pos+1);
	  System.out.println(key + " is " + value);
	  System.out.println("i is " + i);
	  Params.put(key,value);
        } else {
          throw new IllegalArgumentException ("Arguments must be in form \"key=value\" except for final filename");
        }
      } else {
        throw new IllegalArgumentException ("Arguments must be in form \"key=value\" except for final filename");
      }
    }
    // should just leave last argument which is map file name
    mapFileName = args[args.length -1];
  }

  public static void parseMetadataFile(String metadataFileName) throws SAXException,javax.xml.parsers.ParserConfigurationException,IOException, Exception {
    // load in graphic file metadata and make available - use DOM to parse it because it's a small and simple file
    File metadataFile = new File(metadataFileName);
    if (metadataFile.exists() && metadataFile.canRead()){
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      Document doc;
      DocumentBuilder db = dbf.newDocumentBuilder();
      doc = db.parse(metadataFile);
      
      doc.getDocumentElement().normalize();
      enterLog("Processing metadata file; Root element " + doc.getDocumentElement().getNodeName(), 4);
      NodeList mapNodeList = doc.getElementsByTagName("map");
      if (mapNodeList.getLength() > 0){
        enterLog("Processing map metadata file information",4);
        for (int i = 0; i < mapNodeList.getLength(); i++){
	  enterLog("Processing map node " + i,4);
	  Element mapElt = (Element) mapNodeList.item(i); // we're assuming it's an Element, because the schema enforces this
	  String fileNameFromAttr = mapElt.getAttribute("fileName"); // exists by schema
	  NodeList pointNodeList = mapElt.getElementsByTagName("point"); // pointElementList.getLength == 2 by schema
	  Hashtable<String,Float[]> pointHash = new Hashtable<String,Float[]>();
	  for (int j = 0; j <2; j++){
	    Element pointElt = (Element) pointNodeList.item(j);
	    String pointLocation = pointElt.getAttribute("location");
	    Float[] latlong = new Float[2];
	    latlong[0] = Float.parseFloat(pointElt.getAttribute("latitude"));
	    latlong[1] = Float.parseFloat(pointElt.getAttribute("longitude"));
	    pointHash.put(pointLocation,latlong);
	  }
	  enterLog("Stored metadata about file " + fileNameFromAttr, 4);
	  fileMetadataHash.put(fileNameFromAttr,pointHash);
	}
      } else {
	throw new Exception("No map file information in map metadata file");
      }
    } else {
      enterLog("Cannot load metadata file "+metadataFileName,1);
      throw new IOException("Cannot load metadata file "+metadataFileName);
    }
    
    enterLog("Initialisation completed successfully",3);
  }

  public static void writeOutput(RenderedImage outputImage) throws IOException {
    File outFile = new File(Params.get("outputfile") + ".jpg");
    FileOutputStream fos = new FileOutputStream(outFile);
    ImageIO.write(outputImage, "jpeg", fos);
    fos.close();
  }
}
